/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Configuration;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using DaveSexton.DocProject.DocSites.Configuration;
using System.IO;

namespace DaveSexton.DocProject.DocSites
{
	public delegate void KeywordAction(string keyword, float position);

	public static class DocSiteSearch
	{
		#region Public Properties
		public static DocSiteSearchProvider DefaultSearchProvider
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				DocSiteManager.EnsureConfiguration();

				return defaultSearchProvider;
			}
		}

		public static DocSiteSearchProviderCollection SearchProviders
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				DocSiteManager.EnsureConfiguration();

				return searchProviders;
			}
		}

		public static int KeywordCount
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (!indexCreated)
					// This is required so that the admin page displays the correct stats after a setting has been saved
					// since it uses AJAX instead of posting back the entire page.
					return 0;

				DocSiteManager.EnsureConfiguration();

				return defaultSearchProvider.KeywordCount;
			}
		}

		public static int DocumentCount
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (!indexCreated)
					// This is required so that the admin page displays the correct stats after a setting has been saved
					// since it uses AJAX instead of posting back the entire page.
					return 0;

				DocSiteManager.EnsureConfiguration();

				return defaultSearchProvider.DocumentCount;
			}
		}

		public static IEnumerable<KeywordEntry> Keywords
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				DocSiteManager.EnsureConfiguration();

				List<KeywordEntry> keywords = new List<KeywordEntry>(defaultSearchProvider.Keywords);

				keywords.Sort();

				foreach (KeywordEntry keyword in keywords)
					yield return keyword;
			}
		}

		public static string SearchPath
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return HttpContext.Current.Server.MapPath(DocSiteManager.Settings.SearchPath);
			}
		}

		public static DateTime LastCreationDate
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return lastCreationDate;
			}
		}

		public static bool IndexCreated
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return indexCreated;
			}
		}
		#endregion

		#region Private / Protected
		private static readonly Regex titleRegex = new Regex(@"(?<= <title .*? >) .*? (?= </title>)",
			RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace);

		private static readonly Regex keywordsRegex = new Regex(@"(?<= ( > | ^ ) [^<]* ) \b [\w']{" + minimumKeywordLength + @",} \b",
			RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace);

		private static readonly Regex msHelpRegex = new Regex(@"<MSHelp:(?<Node> .+? ) \s+ ( Title=""(?<Title> .+? )"" | Name=""(?<Name> .+? )""  \s+  Value=""(?<Value> .+? )"" )",
			RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		internal static DocSiteSearchProvider defaultSearchProvider;
		internal static DocSiteSearchProviderCollection searchProviders;

		private static bool indexCreated;
		private static DateTime lastCreationDate;

		private static int exactTitleHotKeywordMatchRank;
		private static int partialTitleHotKeywordMatchRank;
		private static int exactTitleKeywordMatchRank;
		private static int partialTitleKeywordMatchRank;
		private static float titleKeywordWeightFactor;
		private static float earlyKeywordWeightFactor;
		private static int minimumKeywordLength;
		private static StringList excludedKeywords, hotTitleKeywords;
		#endregion

		#region Methods
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void UpdateCachedSettings()
		{
			exactTitleHotKeywordMatchRank = DocSiteManager.Settings.SearchExactTitleHotKeywordMatchRank;
			partialTitleHotKeywordMatchRank = DocSiteManager.Settings.SearchPartialTitleHotKeywordMatchRank;
			exactTitleKeywordMatchRank = DocSiteManager.Settings.SearchExactTitleKeywordMatchRank;
			partialTitleKeywordMatchRank = DocSiteManager.Settings.SearchPartialTitleKeywordMatchRank;
			titleKeywordWeightFactor = DocSiteManager.Settings.SearchTitleKeywordWeightFactor;
			earlyKeywordWeightFactor = DocSiteManager.Settings.SearchEarlyKeywordWeightFactor;
			minimumKeywordLength = DocSiteManager.Settings.SearchMinimumKeywordLength;
			excludedKeywords = ListHelper.ParseStringList(DocSiteManager.Settings.SearchExcludedKeywords, ',',
				CleanListOptions.RemoveDuplicates | CleanListOptions.Trim);
			hotTitleKeywords = ListHelper.ParseStringList(DocSiteManager.Settings.SearchHotTitleKeywords, ',',
				CleanListOptions.RemoveDuplicates | CleanListOptions.Trim);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void EnsureIndex()
		{
			if (!indexCreated)
				CreateSearchIndex(SearchPath);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void CreateSearchIndex()
		{
			CreateSearchIndex(SearchPath);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void CreateSearchIndex(string path)
		{
			DocSiteManager.EnsureConfiguration();

			defaultSearchProvider.ClearIndex();

			foreach (string file in System.IO.Directory.GetFiles(path, "*.htm", SearchOption.AllDirectories))
				AnalyzeFile(file);

			lastCreationDate = DateTime.UtcNow;
			indexCreated = true;
		}

		private static void AnalyzeFile(string file)
		{
			string title = null;
			MSHelpEntry msHelp = MSHelpEntry.Empty;

			ForEachWordInHtmlFile(file, ref title, ref msHelp, delegate(string word, float position)
			{
				defaultSearchProvider.AddKeyword(word,
					new IndexEntry(title, file, msHelp, 1, (int) Math.Ceiling((position * -earlyKeywordWeightFactor + earlyKeywordWeightFactor))));
			});

			ForEachWordInHtml(title, delegate(string word, float position)
			{
				defaultSearchProvider.AddKeyword(word,
					new IndexEntry(title, file, msHelp, 1, (int) Math.Ceiling((position * -earlyKeywordWeightFactor + earlyKeywordWeightFactor) * titleKeywordWeightFactor)));
			});
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static QueryExpression CreateQueryExpression(string text)
		{
			if (text == null)
				throw new ArgumentNullException("text");

			DocSiteManager.EnsureConfiguration();

			QueryExpressionFactory factory = new QueryExpressionFactory();

			factory.MinimumKeywordLength = minimumKeywordLength;
			factory.IgnoredWords = excludedKeywords;
			factory.SplitQueryCharacters = defaultSearchProvider.SplitQueryCharacters;
			factory.UnaryQueryOperators = defaultSearchProvider.UnaryQueryOperators;
			factory.QueryOperatorFeedback = defaultSearchProvider.GetQueryOperator;
			factory.DefaultOperator = defaultSearchProvider.DefaultOperator;
			factory.Optimize = defaultSearchProvider.OptimizeQueries;

			return factory.CreateExpression(text);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static IEnumerable<IndexMatch> Search(string text)
		{
			if (text == null)
				throw new ArgumentNullException("text");

			DocSiteManager.EnsureConfiguration();

			EnsureIndex();

			text = text.Substring(0, Math.Min(text.Length, 100));

			QueryExpression query = CreateQueryExpression(text);

			if (query is EmptyQueryExpression)
				yield break;
			else
				foreach (IndexMatch match in defaultSearchProvider.Search(query))
					yield return match;
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static IList<IndexMatch> SearchList(string text)
		{
			return new List<IndexMatch>(Search(text));
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static IList<KeywordEntry> KeywordsList()
		{
			return KeywordsList(null);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static IList<KeywordEntry> KeywordsList(string firstLetter)
		{
			DocSiteManager.EnsureConfiguration();

			EnsureIndex();

			if (string.IsNullOrEmpty(firstLetter))
				return new List<KeywordEntry>(Keywords);

			List<KeywordEntry> list = new List<KeywordEntry>(1024);

			foreach (KeywordEntry entry in Keywords)
				if (entry.Keyword.StartsWith(firstLetter, StringComparison.OrdinalIgnoreCase))
					list.Add(entry);

			return list;
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static ICollection<IndexEntry> KeywordEntriesList(params string[] keywords)
		{
			DocSiteManager.EnsureConfiguration();

			EnsureIndex();

			List<IndexEntry> entries = new List<IndexEntry>(defaultSearchProvider[keywords]);

			entries.Sort(delegate(IndexEntry entry1, IndexEntry entry2)
			{
				return string.CompareOrdinal(entry1.Title, entry2.Title);
			});

			return entries.AsReadOnly();
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static bool IsKeywordIndexed(string keyword)
		{
			if (string.IsNullOrEmpty(keyword))
				return false;

			DocSiteManager.EnsureConfiguration();

			return indexCreated && defaultSearchProvider.HasKeyword(keyword);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "2#")]
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void ForEachWordInHtmlFile(string file, ref string title, ref MSHelpEntry msHelp, KeywordAction action)
		{
			string html = System.IO.File.ReadAllText(file);

			if (string.IsNullOrEmpty(html))
				return;

			Match titleMatch = titleRegex.Match(html);

			if (titleMatch.Success)
				title = titleMatch.Value.Trim();

			if (string.IsNullOrEmpty(title))
				title = Path.GetFileNameWithoutExtension(file);

			msHelp = ParseMSHelpEntry(html, title);

			if (!titleMatch.Success && !string.Equals(title, msHelp.Title, StringComparison.OrdinalIgnoreCase))
				title = msHelp.Title;

			ForEachWordInHtml(html, html.IndexOf("<body", StringComparison.OrdinalIgnoreCase), action);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void ForEachWordInHtml(string value, KeywordAction action)
		{
			ForEachWordInHtml(value, 0, action);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static void ForEachWordInHtml(string value, int startAt, KeywordAction action)
		{
			if (action == null)
				throw new ArgumentNullException("action");

			if (string.IsNullOrEmpty(value))
				return;

			int totalLength = value.Length;

			foreach (Match match in keywordsRegex.Matches(value, startAt))
			{
				string word = match.Value;

				if (word.Length >= minimumKeywordLength && !IsExcludedKeyword(word))
					action(word, (match.Index + 1) / (float) totalLength);
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1807:AvoidUnnecessaryStringCreation", MessageId = "name")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1807:AvoidUnnecessaryStringCreation", MessageId = "attribute")]
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static MSHelpEntry ParseMSHelpEntry(string html, string defaultTitle)
		{
			if (html == null)
				throw new ArgumentNullException("html");

			string title = null, apiType = null, apiLocation = null;
			string apiName = null, locale = null, topicType = null, summary = null;
			bool hasMatch = false;

			foreach (Match match in msHelpRegex.Matches(html))
			{
				hasMatch = true;

				string name = match.Groups["Node"].Value;
				bool hasRLTitle = false;

				switch (name.ToUpperInvariant())
				{
					case "RLTITLE":
						title = match.Groups["Title"].Value;
						hasRLTitle = true;
						break;
					case "TOCTITLE":
						if (!hasRLTitle)
							title = match.Groups["Title"].Value;
						break;
					case "ATTR":
						string attribute = match.Groups["Name"].Value;

						switch (attribute.ToUpperInvariant())
						{
							case "ABSTRACT":
								summary = match.Groups["Value"].Value;
								break;
							case "APITYPE":
								apiType = match.Groups["Value"].Value;
								break;
							case "APILOCATION":
								apiLocation = match.Groups["Value"].Value.Trim();

								if (apiLocation.StartsWith("(", StringComparison.Ordinal) && apiLocation.EndsWith(")", StringComparison.Ordinal))
									apiLocation = apiLocation.Substring(1, apiLocation.Length - 2);
								break;
							case "APINAME":
								apiName = match.Groups["Value"].Value;
								break;
							case "LOCALE":
								locale = match.Groups["Value"].Value;
								break;
							case "TOPICTYPE":
								topicType = match.Groups["Value"].Value;
								break;
						}
						break;
				}
			}

			if (hasMatch)
			{
				if (string.IsNullOrEmpty(title))
					title = defaultTitle;

				return new MSHelpEntry(title, apiLocation, apiType, apiName, locale, topicType, summary);
			}
			else
				return MSHelpEntry.Empty;
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static bool IsExcludedKeyword(string word)
		{
			return excludedKeywords.Contains(word);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static bool IsHotTitleKeyword(string word)
		{
			return hotTitleKeywords.Contains(word);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static int GetAdditionalRankForTitleMatch(IndexEntry entry, string word)
		{
			string title = entry.Title;

			if (!string.IsNullOrEmpty(title))
			{
				if (Regex.IsMatch(title, @"\b" + Regex.Escape(word) + @"\b", RegexOptions.IgnoreCase))
					return (IsHotTitleKeyword(word)) ? exactTitleHotKeywordMatchRank : exactTitleKeywordMatchRank;
				else if (title.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) > -1)
					return (IsHotTitleKeyword(word)) ? partialTitleHotKeywordMatchRank : partialTitleKeywordMatchRank;
			}

			return 0;
		}
		#endregion
	}
}
